package generic.net;

import generic.net.Response.Status;
import generic.net.observers.BroadcastObserver;
import generic.net.observers.MessageDispatcher;
import generic.net.observers.PeerConnectDispatcher;
import generic.net.observers.PeerDisconnectDispatcher;
import generic.net.observers.RequestDispatcher;
import java.io.IOException;
import java.net.Socket;

/**
 * A Peer enables Bi-directional communication between a two applications.
 *
 * @author Adam Scarr <scarr.adam@gmail.com>
 */
public class Peer implements Runnable, BroadcastObserver {
    private boolean connected = false;
    private Socket socket;
    private MessageOutputStream outStream;
    private MessageInputStream inStream;
    private RequestManager requestManager = new RequestManager();
    private MessageDispatcher messageDispatcher = new MessageDispatcher();
    private RequestDispatcher requestDispatcher = new RequestDispatcher();
    private PeerDisconnectDispatcher peerDisconnectDispatcher = new PeerDisconnectDispatcher();
    private PeerConnectDispatcher peerConnectDispatcher = new PeerConnectDispatcher();
    private Thread thread;

    /**
     * Creates a connection to another peer via the given socket.
     * @param socket The socket to connect to.
     */
    public void connect(Socket socket) {
        if (connected) {
            System.out.println("[PEER] Peer is already connected...");
        } else {
            this.socket = socket;
        }

        try {
            outStream = new MessageOutputStream(socket.getOutputStream());
            //outStream = new XMLEncoder(System.out);
            inStream = new MessageInputStream(socket.getInputStream());
        } catch (IOException ex) {
            System.out.println("[PEER] Could not create read/write from socket.");
        }

        connected = true;
        System.out.println("[PEER] Connected.");

        thread = new Thread(this, "Peer-messageWait");
        thread.start();

        peerConnectDispatcher.notifyPeerConnect(this);
    }

    /**
     * Closes the connection to the peer.
     */
    public void disconnect() {
        try {
            socket.close();
        } catch (IOException ex) {
            System.out.println("[PEER] Could not close connection, perhaps it was lost?");
        }
        connected = false;
    }

    /**
     * Receive messages from the remote peer.
     */
    public void run() {
        while (connected) {
            Message message = inStream.read();

            if (message == null) {
                System.out.println("[PEER] Connection closed while waiting for message");
                connected = false;
                break;
            } else {
                //System.out.println("[PEER] Received message: " + message.toString());

                // There are a few special types of messages we know about.
                // So handle them if we can, otherwise pass it on to
                // messageReceived for the implementing class to sort out.
                if (message.getType().equals("RESPONSE")) {
                    requestManager.addResponse((Response)message);
                } else if (message.getType().equals("REQUEST")) {
                    requestDispatcher.notifyRequestReceived(this, (Request)message);
                } else {
                    messageDispatcher.notifyMessageReceived(this, message);
                }
            }
        }

        peerDisconnectDispatcher.notifyPeerDisconnect(this);
    }

    public boolean isConnected() {
        return connected;
    }

    public Thread getThread() {
        return thread;
    }

    /**
     * Send a message to the remote peer.
     * @param message   The message to send.
     */
    public void send(Message message) {
        if (!connected) {
            System.out.println("[PEER] Not connected, cannot send message " + message.toString());
            return;
        }

        try {
            outStream.write(message);
        } catch (IOException ex) {
            System.out.println("[PEER] " + ex);
        }
    }

    /**
     * Sends a request to the remote peer, and returns a PendingResponse.
     * @param       request   The request
     * @return      A response from the remote peer.
     */
    public PendingResponse sendRequest(String service, String requestType, Object ... payload) {
        Request request = new Request(service, requestType, payload);
        PendingResponse pendingResponse = requestManager.addRequest(request);
        send(request);

        return pendingResponse;
    }

    /**
     * Sends a request to the remote peer, and returns a PendingResponse.
     * @param       request   The request
     * @return      A response from the remote peer.
     */
    public PendingResponse sendRequest(String service, String requestType) {
        Request request = new Request(service, requestType);
        send(request);
        PendingResponse pendingResponse = requestManager.addRequest(request);

        return pendingResponse;
    }

    /**
     * Sends a response to the given request.
     * @param request   The request you are responding too
     * @param response  The response.
     */
    public void sendResponse(Request request, Status status) {
        Response response = new Response(request);
        response.setStatus(status);
        //System.out.println("[PEER] Sending response #" + response.getID());
        send(response);
    }

    /**
     * Sends a response to the given request.
     * @param request   The request you are responding too
     * @param response  The response.
     */
    public void sendResponse(Request request, Object payload, Status status) {
        Response response = new Response(request, payload);
        response.setStatus(status);
        //System.out.println("[PEER] Sending response #" + response.getID());
        send(response);
    }

    /**
     * Sends a response to the given request.
     * @param request   The request you are responding too
     * @param response  The response.
     */
    public void sendResponse(Request request, Object payload) {
        Response response = new Response(request, payload);
        //System.out.println("[PEER] Sending response #" + response.getID());
        send(response);
    }

    /**
     * @return The message dispatcher
     */
    public MessageDispatcher getMessageDispatcher() {
        return messageDispatcher;
    }

    /**
     * @return The Peer disconnect dispatcher
     */
    public PeerDisconnectDispatcher getPeerDisconnectDispatcher() {
        return peerDisconnectDispatcher;
    }

    /**
     * @return The Request dispatcher
     */
    public RequestDispatcher getRequestDispatcher() {
        return requestDispatcher;
    }

    /**
     * @return The Peer connect dispatcher
     */
    public PeerConnectDispatcher getPeerConnectDispatcher() {
        return peerConnectDispatcher;
    }

}
